package com.duomi.lang;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

class ReflectUtils {

    private ReflectUtils() {
    }

    private static final Map<Key, Object> sCache = new HashMap<>();

    static Object invokeConstructor(Class<?> clazz, List<Object> arguments) {
        try {
            if (arguments.isEmpty()) {
                return clazz.newInstance();
            }
            final Constructor<?> constructor = findConstructor(clazz, arguments);
            return constructor.newInstance(arguments.toArray());
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new DuomiEvalException("ReflectFailed", e);
        }
    }

    private static Constructor<?> findConstructor(Class<?> clazz, List<Object> arguments) {
        final Class[] argTypes = transArgsToTypes(arguments);
        return (Constructor<?>) getOrPut(sCache, Key.ofConstructor(clazz, argTypes), () -> findConstructor(clazz, argTypes));
    }

    private static Constructor<?> findConstructor(Class<?> clazz, Class[] argTypes) {
        Exception error;
        try {
            return clazz.getDeclaredConstructor(argTypes);
        } catch (NoSuchMethodException e) {
            // try for generic
            error = e;
        }

        final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            if (isArgsMatchWithParameters(argTypes, constructor.getParameterTypes())) {
                return constructor;
            }
        }
        throw new DuomiEvalException("ReflectFailed", error);
    }

    static Object invokeMethod(Object host, String name, List<Object> arguments) {
        try {
            final Method method = findMethod(getClass(host), name, arguments);
            if (arguments.isEmpty()) {
                return method.invoke(host);
            }
            return method.invoke(host, arguments.toArray());
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new DuomiEvalException("ReflectFailed", e);
        }
    }

    private static Class<?> getClass(Object host) {
        if (host instanceof Class) {
            return ((Class) host);
        }
        return host.getClass();
    }

    private static Method findMethod(Class<?> clazz, String name, List<Object> arguments) {
        final Class[] argTypes = transArgsToTypes(arguments);
        return (Method) getOrPut(sCache, Key.ofMethod(clazz, name, argTypes), () -> findMethod(clazz, name, argTypes));
    }

    private static Method findMethod(Class<?> clazz, String name, Class[] argTypes) {
        Exception error;
        try {
            return clazz.getDeclaredMethod(name, argTypes);
        } catch (NoSuchMethodException e) {
            error = e;
        }

        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(name) && isArgsMatchWithParameters(argTypes, method.getParameterTypes())) {
                return method;
            }
        }
        throw new DuomiEvalException("ReflectFailed", error);
    }

    static Object getField(Object host, Field field) {
        try {
            field.setAccessible(true);
            return field.get(host);
        } catch (IllegalAccessException e) {
            throw new DuomiEvalException("ReflectFailed", e);
        }
    }

    static void setField(Object host, Field field, Object value) {
        try {
            field.setAccessible(true);
            field.set(host, value);
        } catch (IllegalAccessException e) {
            throw new DuomiEvalException("ReflectFailed", e);
        }
    }

    static Field findField(Class<?> clazz, String name) {
        try {
            return clazz.getDeclaredField(name);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    private static boolean isArgsMatchWithParameters(Class[] argTypes, Class<?>[] parameterTypes) {
        if (argTypes.length != parameterTypes.length) {
            return false;
        }
        for (int i = 0; i < argTypes.length; i++) {
            final Class argType = argTypes[i];
            final Class<?> parameterType = parameterTypes[i];
            if (!isTypeMatch(parameterType, argType)) {
                return false;
            }
        }
        return true;
    }

    private static boolean isTypeMatch(Class parameterType, Class argType) {
        if (parameterType.isAssignableFrom(argType)) {
            return true;
        }
        if (parameterType == byte.class && argType.equals(Byte.class)) {
            return true;
        }
        if (parameterType == short.class && argType.equals(Short.class)) {
            return true;
        }
        if (parameterType == int.class && argType.equals(Integer.class)) {
            return true;
        }
        if (parameterType == long.class && argType.equals(Long.class)) {
            return true;
        }
        if (parameterType == char.class && argType.equals(Character.class)) {
            return true;
        }
        if (parameterType == float.class && argType.equals(Float.class)) {
            return true;
        }
        if (parameterType == double.class && argType.equals(Double.class)) {
            return true;
        }
        return false;
    }

    private static Class[] transArgsToTypes(List<Object> args) {
        final Class[] types = new Class[args.size()];
        for (int i = 0; i < args.size(); i++) {
            types[i] = args.get(i).getClass();
        }
        return types;
    }

    interface Supplier<T> {

        /**
         * Gets a result.
         *
         * @return a result
         */
        T get();
    }

    @SuppressWarnings("SameParameterValue")
    private static <K, V> V getOrPut(Map<K, V> map, K key, Supplier<V> valueSupplier) {
        if (map.containsKey(key)) {
            return map.get(key);
        }
        final V value = valueSupplier.get();
        map.put(key, value);
        return value;
    }


    private static final int TYPE_CONSTRUCTOR = 1;
    private static final int TYPE_METHOD = 2;

    static class Key {
        final Class<?> clazz;
        final int type;
        final String name;
        final Class[] argTypes;

        static Key ofConstructor(Class clazz, Class[] argTypes) {
            return new Key(clazz, TYPE_CONSTRUCTOR, "", argTypes);
        }

        static Key ofMethod(Class clazz, String name, Class[] argTypes) {
            return new Key(clazz, TYPE_METHOD, name, argTypes);
        }

        Key(Class<?> clazz, int type, String name, Class[] argTypes) {
            this.clazz = clazz;
            this.type = type;
            this.name = name;
            this.argTypes = argTypes;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Key key = (Key) o;
            return type == key.type &&
                    Objects.equals(clazz, key.clazz) &&
                    Objects.equals(name, key.name) &&
                    Arrays.equals(argTypes, key.argTypes);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(clazz, type, name);
            result = 31 * result + Arrays.hashCode(argTypes);
            return result;
        }
    }
}
